package org.spring.blog.service.impl;

import com.google.gson.Gson;
import com.wf.captcha.ArithmeticCaptcha;
import com.wf.captcha.GifCaptcha;
import com.wf.captcha.SpecCaptcha;
import com.wf.captcha.base.Captcha;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.extern.slf4j.Slf4j;
import org.spring.blog.dao.RefreshTokenDao;
import org.spring.blog.dao.SettingsDao;
import org.spring.blog.dao.UserDao;
import org.spring.blog.dao.UserNoPasswordDao;
import org.spring.blog.manager.CountDownLatchManager;
import org.spring.blog.pojo.RefreshToken;
import org.spring.blog.pojo.Setting;
import org.spring.blog.pojo.User;
import org.spring.blog.response.ResponseResult;
import org.spring.blog.response.ResponseState;
import org.spring.blog.service.IUserService;
import org.spring.blog.utils.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.transaction.Transactional;
import java.awt.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
@Transactional
public class UserService implements IUserService {

    @Autowired
    CountDownLatchManager countDownLatchManager;

    @Autowired
    UserNoPasswordDao userNoPasswordDao;

    @Autowired
    Gson gson;

    @Autowired
    RefreshTokenDao refreshTokenDao;

    @Autowired
    TaskService taskService;

    @Autowired
    RedisUtil redisUtil;

    public static final int[] captcha_font_types = {
            Captcha.FONT_1,
            Captcha.FONT_2,
            Captcha.FONT_3,
            Captcha.FONT_4,
            Captcha.FONT_5,
            Captcha.FONT_6,
            Captcha.FONT_7,
            Captcha.FONT_8,
            Captcha.FONT_9,
            Captcha.FONT_10,
    };


    /**
     * 生成id的类
     */
    @Autowired
    SnowflakeIdWorker snowflakeIdWorker;

    /**
     * 用户数据服访问层
     */
    @Autowired
    UserDao userDao;

    @Autowired
    SettingsDao settingsDao;

    @Autowired
    BCryptPasswordEncoder bCryptPasswordEncoder;

    @Autowired
    Random random;


    /**
     * 修改密码
     * 步骤
     * 1.用户登录
     * 2.使用用户的邮箱发送验证码
     * 3.用户填写验证码，和新密码
     * 4.服务器验证验证码和密码
     * 5.返回用户信息和更改服务器状态
     *
     * @param _verifyCode 邮箱收到的验证码值
     * @param _user       用户信息
     * @return 自定义返回对象
     */
    @Override
    public ResponseResult updatePasswordByUserEmail(String _verifyCode, User _user) {

        // check data
        String email = _user.getEmail();

        String redisVerifyCode = (String) redisUtil.get(constant.REDIS_KEY_EMAIL_SEND_VERIFY_CONTENT + email);

        if (redisVerifyCode == null) {
            return ResponseResult.failed(ResponseState.verify_is_over_message, ResponseState.verify_is_over_code);
        }

        if (redisVerifyCode.equals(_verifyCode)) {
            // change data
            redisUtil.del(constant.REDIS_KEY_EMAIL_SEND_VERIFY_CONTENT + email);
        } else {
            return ResponseResult.failed(ResponseState.verify_value_error_message, ResponseState.verify_value_error_code);
        }


        int i = userDao.updatePasswordByEmail(bCryptPasswordEncoder.encode(_user.getPassword()), email);

        return i > 0 ? ResponseResult.success() : ResponseResult.failed();
    }

    /**
     * 修改邮箱
     * 1.登录状态 2.邮箱唯一
     *
     * @param _email      新的邮箱地址
     * @param _verifyCode 邮箱验证码
     * @return 自定义返回对象
     */
    @Override
//    @Transactional
    public ResponseResult updateEmail(String _email, String _verifyCode) {
        // check env
        User userIsLogin = checkUserIsLogin();

        if (userIsLogin == null) {
            return ResponseResult.failed(ResponseState.user_not_login_message, ResponseState.user_not_login_code);
        }

        // check data
        String redisVerifyCode;
        redisVerifyCode = (String) redisUtil.get(constant.REDIS_KEY_EMAIL_SEND_VERIFY_CONTENT + _email);

        if (redisVerifyCode == null) {
            return ResponseResult.failed(ResponseState.verify_is_over_message, ResponseState.verify_is_over_code);
        }

        if (redisVerifyCode.equals(_verifyCode)) {
            redisUtil.del(constant.REDIS_KEY_EMAIL_SEND_VERIFY_CONTENT + _email);
        } else {
            return ResponseResult.failed(ResponseState.verify_value_error_message, ResponseState.verify_value_error_code);
        }

        ResponseResult responseResult = checkEmailUnique(_email, userIsLogin.getId());

        if (responseResult.getCode() == ResponseState.email_not_unique_code) {
            return responseResult;
        }

        int i = userDao.updateEmailById(_email, userIsLogin.getId());

        return i > 0 ? ResponseResult.success() : ResponseResult.failed();
    }

    /**
     * 检查用户名的唯一性
     * ///////// 使用场景
     * 1. 用户注册 2. 修改username 昵称
     *
     * @param _username 用户输入的用户名
     * @return 自定义的返回对象
     */
    @Override
    public ResponseResult checkUsernameUnique(String _username, String _ownId) {
        User oneByUserName = userDao.findOneByUserName(_username);

        if (oneByUserName == null) {
            return ResponseResult.failed(ResponseState.username_is_unique__message, ResponseState.username_is_unique_code);
        } else {
            User userIsLogin = userDao.findOneById(_ownId);

            if (userIsLogin != null) {
                if (userIsLogin.getUserName().equals(_username)) {
                    return ResponseResult.failed(ResponseState.username_is_unique__message, ResponseState.username_is_unique_code);
                }
            }
            return ResponseResult.failed(ResponseState.username_not_unique__message, ResponseState.username_not_unique_code);
        }
    }

    /**
     * 检查邮箱是否存在
     * ///////// 使用场景
     * 1. 用户注册 2. 修改邮箱
     * 这两个场景不影响 接口处理问题
     * <p>
     * 我们也可以让前端处理场景的问题 我们就返回邮箱是否已经存在
     *
     * @param _email 邮箱地址
     * @return 自定义的返回对象
     */
    @Override
    public ResponseResult checkEmailUnique(String _email, String _ownId) {
        User oneByEmail = userDao.findOneByEmail(_email);

        if (oneByEmail == null) {
            return ResponseResult.failed(ResponseState.email_is_unique_message, ResponseState.email_is_unique_code);
        } else {
            User userIsLogin = userDao.findOneById(_ownId);

            if (userIsLogin != null) {
                if (userIsLogin.getEmail().equals(_email)) {
                    return ResponseResult.failed(ResponseState.email_is_unique_message, ResponseState.email_is_unique_code);
                }
            }
            return ResponseResult.failed(ResponseState.email_not_unique_message, ResponseState.email_not_unique_code);
        }
    }

    /**
     * 用户登录
     *
     * @param _captcha 人类验证码信息
     * @param _user    用户信息
     * @return 返回结果信息类
     */
    @Override
    public ResponseResult login(String _captcha, User _user, String _from) {
        // check devices state
        if (_from == null) {
            _from = from_mobile;
        }
        switch (_from) {
            case from_mobile:
            case from_pc:
                break;
            default:
                // 设置一个默认设备
                _from = from_mobile;
        }
        // check user state
        // 防止重复登录 但是用户想登录别人的账号就不行了
        // 需要增加判断 当前登录账号和新登录的账号不一样
        User userIsLogin = checkUserIsLogin();

        if (userIsLogin != null) {
            if (userIsLogin.getUserName().equals(_user.getUserName()) || userIsLogin.getEmail().equals(_user.getUserName())) {
                return ResponseResult.failed(ResponseState.user_is_login_message, ResponseState.user_is_login_code);
            }
        }

        // check data

        // Check if the username|email is empty
        String userName = _user.getUserName();
        if (TextUtil.isEmpty(userName)) {
            return ResponseResult.failed(ResponseState.username_or_email_cant_empty_message, ResponseState.username_or_email_cant_empty_code);
        }

        // Check if the password is empty
        String password = _user.getPassword();

        if (TextUtil.isEmpty(password)) {
            return ResponseResult.failed(ResponseState.password_cant_empty_message, ResponseState.password_cant_empty_code);
        }

        // Check if the user is registered
        User oneByUserName = userDao.findOneByUserName(userName);

        if (oneByUserName == null) {

            oneByUserName = userDao.findOneByEmail(userName);

            if (oneByUserName == null) {
                return ResponseResult.failed(ResponseState.username_or_email_error_message, ResponseState.username_or_email_error_code);
            }
        }

        // The user is registered,
        // so we can check if the password is right
        boolean matches = bCryptPasswordEncoder.matches(password, oneByUserName.getPassword());
        if (!matches) {
            return ResponseResult.failed(ResponseState.password_error_message, ResponseState.password_error_code);
        }

        // Check if the user's state is normal
        if (!oneByUserName.getState().equals(constant.db_user_normal_state)) {
            return ResponseResult.failed(ResponseState.account_is_ban_message, ResponseState.account_is_ban_code);
        }

        String _captchaKey = CookieUtil.getCookieValue(getRequest(), cookie_last_captcha_id);
        String captchaValue = (String) redisUtil.get(constant.REDIS_KEY_CAPTCHA_CONTENT + _captchaKey);

        if (captchaValue == null) {
            return ResponseResult.failed(ResponseState.captcha_is_over_message, ResponseState.captcha_is_over_code);
        }

        if (captchaValue.equals(_captcha)) {
            redisUtil.del(constant.REDIS_KEY_CAPTCHA_CONTENT + _captchaKey);
        } else {
            return ResponseResult.failed(ResponseState.captcha_value_error_message, ResponseState.captcha_value_error_code);
        }

        // Check data success, All of user input is right
        createToken(getResponse(), oneByUserName, _from);

        return ResponseResult.success();
    }

    /**
     * 用户登录成功生成 用户数据 和返回数据的方法
     *
     * @param _response      请求体 用于使用cookie 返回数据
     * @param _oneByUserName 用户对象
     */
    private void createToken(HttpServletResponse _response, User _oneByUserName, String _from) {
        // delete cache
        // 删除老的登录数据
        String oldTokenKey = CookieUtil.getCookieValue(getRequest(), constant.cookie_token_key);
        RefreshToken oldRefreshToken = refreshTokenDao.findOneByUserID(_oneByUserName.getId());

        if (oldRefreshToken != null) {
            switch (_from) {
                case from_pc:
                    redisUtil.del(constant.REDIS_TOKEN_KEY + oldRefreshToken.getTokenKey());
                    refreshTokenDao.deletePcTokenKey(oldTokenKey);
                    break;
                case from_mobile:
                    redisUtil.del(constant.REDIS_TOKEN_KEY + oldRefreshToken.getMobileTokenKey());
                    refreshTokenDao.deleteMobileToken(oldTokenKey);
                    break;
            }
        }

        // complete data
        Map<String, Object> claims = ClaimsUtil.user2Claims(_oneByUserName, _from);

        String token = JWTUtil.createToken(claims, constant.jwt_token_keep_time);

        // create the md5(token)'s key by the spring's utils
        String md5DigestAsHex = _from + DigestUtils.md5DigestAsHex(token.getBytes(StandardCharsets.UTF_8));

        RefreshToken refreshTokenByUserID = refreshTokenDao.findOneByUserID(_oneByUserName.getId());

        // 创建refresh token 为了当 redis 里的 token 过期了 用户也不用再次登录
        String refreshToken = JWTUtil.createRefreshToken(_oneByUserName.getId(), constant.jwt_refreshToken_keep_time);

        if (refreshTokenByUserID == null) {
            refreshTokenByUserID = new RefreshToken().setId(String.valueOf(snowflakeIdWorker.nextId()))
                    .setUserID(_oneByUserName.getId())
                    .setCreateTime(new Date());
        }

        // change database state
        refreshTokenByUserID.setRefreshToken(refreshToken)
                .setUpdateTime(new Date());

        if (_from.equals(from_pc)) {
            refreshTokenByUserID.setTokenKey(md5DigestAsHex);
        } else if (_from.equals(from_mobile)) {
            refreshTokenByUserID.setMobileTokenKey(md5DigestAsHex);
        }

        refreshTokenDao.save(refreshTokenByUserID);

        // change user state be login;
        // change redis database state
        redisUtil.set(constant.REDIS_TOKEN_KEY + md5DigestAsHex, token, constant.redis_token_keep_time);

        // return data
        // return the token's key by put it into cookie
        CookieUtil.setUpCookie(_response, constant.cookie_token_key, md5DigestAsHex, constant.cookie_token_key_keep_time);
    }

    /**
     * 初始化管理员账号
     *
     * @param _user    用户对象
     * @param _request 请求体
     * @return 自定义的返回对象
     */
    @Override
    public ResponseResult initManagerAccount(User _user, HttpServletRequest _request) {

        // 检查是否有初始化
        Setting managerAccountState = settingsDao.findOneByKey(LRConstants.settings.MANAGER_ACCOUNT_INIT);

        if (managerAccountState != null) {
            return ResponseResult.failed(ResponseState.admin_is_register_message, ResponseState.admin_is_register_code);
        }

        // 检查数据
        if (TextUtil.isEmpty(_user.getUserName())) {
            return ResponseResult.failed(ResponseState.username_cant_empty_message, ResponseState.username_cant_empty_code);
        }

        if (TextUtil.isEmpty(_user.getEmail())) {
            return ResponseResult.failed(ResponseState.email_cant_empty_message, ResponseState.email_cant_empty_code);
        }

        if (TextUtil.isEmpty(_user.getPassword())) {
            return ResponseResult.failed(ResponseState.password_cant_empty_message, ResponseState.password_cant_empty_code);
        }

        // 补充数据
        _user.setId(String.valueOf(snowflakeIdWorker.nextId()))
                .setRoles(constant.DB_ROLE_ADMIN)
                .setAvatar(constant.DB_DEFAULT_AVATAR)
                .setState(constant.db_user_normal_state)
                .setLoginIp(_request.getRemoteAddr())
                .setRegIp(_request.getRemoteAddr())
                .setCreateTime(new Date())
                .setUpdateTime(new Date());


        Setting setting = new Setting()
                .setId(String.valueOf(snowflakeIdWorker.nextId()))
                .setCreateTime(new Date())
                .setUpdateTime(new Date())
                .setValue(LRConstants.settings.DEFAULT_STATE)
                .setKey(LRConstants.settings.MANAGER_ACCOUNT_INIT);


        // 加密密码
        String password = _user.getPassword();
        password = bCryptPasswordEncoder.encode(password);
        _user.setPassword(password);

        // save data
        userDao.save(_user);
        settingsDao.save(setting);
        return ResponseResult.success();
    }

    /**
     * 创建验证码的方法
     *
     * @param _response 网络返回体
     * @throws IOException         io错误
     * @throws FontFormatException 定义的字体id不存在
     */
    @Override
    public void createCaptcha(HttpServletResponse _response) throws IOException, FontFormatException {

        String lastId = CookieUtil.getCookieValue(getRequest(), cookie_last_captcha_id);

        String key;

        // check data
        if (TextUtil.isEmpty(lastId)) {
            key = String.valueOf(snowflakeIdWorker.nextId());
        } else {
            key = lastId;
        }


        // 设置请求头为输出图片类型
        _response.setContentType("image/gif");
        _response.setHeader("Pragma", "No-cache");
        _response.setHeader("Cache-Control", "No-cache");
        _response.setDateHeader("Expires", 0L);


        // create captcha
        Captcha captcha;
        switch (random.nextInt(3)) {
            case 0:
                // 三个参数为 宽，高，位数
                captcha = new SpecCaptcha(130, 48, 4);

                break;
            case 1:
                captcha = new GifCaptcha(130, 48);
                break;
            case 2:
                // 算数类型验证码
                captcha = new ArithmeticCaptcha(130, 48);
                // 几位数运算，默认两位
                captcha.setLen(2);
                // 获取计算结果
                captcha.text();
                break;
            default:
                throw new IllegalStateException("Unexpected value: " + random.nextInt(3));
        }

        // set text font
        captcha.setFont(captcha_font_types[random.nextInt(captcha_font_types.length)]);

        // 设置验证码类型 纯数字，纯字母， 字母数字混合
        captcha.setCharType(Captcha.TYPE_DEFAULT);

        String content = captcha.text().toLowerCase(Locale.ENGLISH);
        // save captcha
        // 1.put the captcha into session
//        _request.getSession().setAttribute("captcha", content);
        // 2.you also can put the [content] into a database
        redisUtil.set(constant.REDIS_KEY_CAPTCHA_CONTENT + key, content, constant.redis_captcha_code_keep_time);
        CookieUtil.setUpCookie(_response, cookie_last_captcha_id, key);

        System.out.println(captcha.getClass().getSimpleName());

        captcha.out(_response.getOutputStream());
    }

    /**
     * 发送邮箱的验证码
     *
     * @param _request 请求体
     * @param _email   邮箱地址
     * @return 自定义的返回对象
     * <p>
     * 使用场景：
     * <p>
     * 1.注册
     * findOneByEmail true 提示邮箱已经注册，false 使用用户输入的邮箱发送验证码 注册
     * 2.找回密码
     * findOneByEmail true 发送验证码 找回密码，false 提示邮箱未绑定用户
     * 3.修改邮箱 （新的邮箱)
     * findOneByEmail true 提示邮箱已经绑定用户，false 使用用户输入的邮箱发送验证码 修改邮箱
     * <p>
     * 方法要求：
     * <p>
     * 同一个邮箱间隔要15秒
     * 同一个ip最多100此，短信100条 1小时内
     * 验证码保存4分钟
     */
    @Override
    public ResponseResult sendEmailVerifyCode(HttpServletRequest _request, String _email) {
        // check env

        // check data
        // check send email time
        String remoteAddr = _request.getRemoteAddr();

        if (remoteAddr != null) {
            System.out.println(remoteAddr);
            remoteAddr = remoteAddr.replaceAll(":", "-");
        }

        System.out.println(remoteAddr);

        String isSendTime = (String) redisUtil.get(constant.REDIS_KEY_EMAIL_SEND_IP + remoteAddr);

        if (isSendTime != null) {
            if (Long.parseLong(isSendTime) > constant.redis_max_email_send_num) {
                // 发的太多
                return ResponseResult.failed(ResponseState.send_verify_too_much_message, ResponseState.send_verify_too_much_code);
            }
        }

        Object hasSendEmail = redisUtil.get(constant.REDIS_KEY_EMAIL_SEND_ADDRESS + _email);

        if (hasSendEmail != null) {
            return ResponseResult.failed(ResponseState.send_verify_too_fast_message, ResponseState.send_verify_too_fast_code);
        }


        // check the email is ok
        if (!TextUtil.isEmailAddress(_email)) {
            return ResponseResult.failed(ResponseState.email_form_error_message, ResponseState.email_form_error_code);
        }


        // complete data
        // 随机生成一个4位数内的验证码
        int code = random.nextInt(9999);

        System.out.println("验证码" + code);

        taskService.sendCodeEmail(_email, code);


        // change database state
        // save data
        // save send times
        // 玩家在一定时间第一次发送邮件
        if (isSendTime == null) {
            isSendTime = "0";
        }

        isSendTime = String.valueOf(Integer.parseInt(isSendTime) + 1);

        redisUtil.set(constant.REDIS_KEY_EMAIL_SEND_IP + remoteAddr, isSendTime, constant.redis_email_limit_time);

        // save send time 保存发送验证码的flag到redis database, 相当与一个计时器
        redisUtil.set(constant.REDIS_KEY_EMAIL_SEND_ADDRESS + _email, "send", constant.redis_email_limit_interval_time);

        // save verify code 保存验证码到redis database
        redisUtil.set(constant.REDIS_KEY_EMAIL_SEND_VERIFY_CONTENT + _email, String.valueOf(code), constant.redis_verify_code_keep_time);

        return ResponseResult.success();
    }


    /**
     * 用户注册
     *
     * @param _request     请求体
     * @param _user        用户对象
     * @param _verifyCode  邮箱验证码
     * @param _captchaCode 人类验证码
     * @return 自定义的返回对象
     */
    @Override
    public ResponseResult register(HttpServletRequest _request, User _user, String _verifyCode, String _captchaCode) {
        // check data
        // 检查用户是否已经注册
        String userName = _user.getUserName();
        if (TextUtil.isEmpty(userName)) {
            return ResponseResult.failed(ResponseState.username_cant_empty_message, ResponseState.username_cant_empty_code);
        }

        User daoOneByUserName = userDao.findOneByUserName(userName);

        if (daoOneByUserName != null) {
            return ResponseResult.failed(ResponseState.username_not_unique__message, ResponseState.username_not_unique_code);
        }

        String userEmail = _user.getEmail();

        if (TextUtil.isEmpty(userEmail)) {
            return ResponseResult.failed(ResponseState.email_cant_empty_message, ResponseState.email_cant_empty_code);
        }

        // 检查邮箱格式是否正确
        if (!TextUtil.isEmailAddress(userEmail)) {
            return ResponseResult.failed(ResponseState.email_form_error_message, ResponseState.email_form_error_code);
        }

        // 检查该邮箱是否已经注册
        if (userDao.findOneByEmail(userEmail) != null) {
            return ResponseResult.failed(ResponseState.email_not_unique_message, ResponseState.email_not_unique_code);
        }

        // 检查人类验证码是否正确
        String _captchaKey = CookieUtil.getCookieValue(getRequest(), cookie_last_captcha_id);
        String captchaCode = (String) redisUtil.get(constant.REDIS_KEY_CAPTCHA_CONTENT + _captchaKey);

        if (TextUtil.isEmpty(captchaCode)) {
            return ResponseResult.failed(ResponseState.captcha_is_over_message, ResponseState.captcha_is_over_code);
        }

        if (captchaCode.equals(_captchaCode)) {
            redisUtil.del(constant.REDIS_KEY_CAPTCHA_CONTENT + _captchaKey);
        } else {
            return ResponseResult.failed(ResponseState.captcha_value_error_message, ResponseState.captcha_value_error_code);
        }

        // 检测加密 密码
        String password = _user.getPassword();

        if (TextUtil.isEmpty(password)) {
            return ResponseResult.failed(ResponseState.password_cant_empty_message, ResponseState.password_cant_empty_code);
        }

        // 检查邮箱验证码是否正确
        String emailVerifyCode = (String) redisUtil.get(constant.REDIS_KEY_EMAIL_SEND_VERIFY_CONTENT + userEmail);

        if (TextUtil.isEmpty(emailVerifyCode)) {
            return ResponseResult.failed(ResponseState.verify_is_over_message, ResponseState.verify_is_over_code);
        }

        if (emailVerifyCode.equals(_verifyCode)) {
            // change database state
            // 验证正确，删除redis database's data
            redisUtil.del(constant.REDIS_KEY_EMAIL_SEND_VERIFY_CONTENT + userEmail);
        } else {
            return ResponseResult.failed(ResponseState.verify_value_error_message, ResponseState.verify_value_error_code);
        }

        _user.setPassword(bCryptPasswordEncoder.encode(password));
        // complete data
        _user.setRegIp(_request.getRemoteAddr())
                .setLoginIp(_request.getRemoteAddr())
                .setUpdateTime(new Date())
                .setCreateTime(new Date())
                .setAvatar(constant.DB_DEFAULT_AVATAR)
                .setRoles(constant.DB_ROLE_NORMAL)
                .setState(constant.db_user_normal_state)
                .setId(String.valueOf(snowflakeIdWorker.nextId()));
        // change user state
        userDao.save(_user);

        return ResponseResult.success();
    }

    /**
     * 检查用户是否有登录
     * 判断标准：
     * 1.用户设备持有 cookie_token_key : tokenKey
     * 2.redis 数据库有 redis_token_key: token || mysql的 tb_refresh_token 中存在 数据的 token_key的值与用户持有的相同
     *
     * @return 用户对象
     */
    @Override
    public User checkUserIsLogin() {
        HttpServletRequest _request = getRequest();
        HttpServletResponse _response = getResponse();

        // check data
        String tokenKey = CookieUtil.getCookieValue(_request, constant.cookie_token_key);

        // 未登录返回
        if (tokenKey == null) {
//            System.out.println("用户没有登录过");
            return null;
        }

        String from = tokenKey.split("_")[0] + "_";

        String token = (String) redisUtil.get(constant.REDIS_TOKEN_KEY + tokenKey);

        if (token != null) {
            Claims claims;
            try {
                claims = JWTUtil.parseJWT(token);
                return ClaimsUtil.claims2User(claims);
            } catch (ExpiredJwtException _e) {
                _e.printStackTrace();
                System.out.println("缓存里没有用户的信息");
            }

        }
        RefreshToken oneByTokenKey;
        // The token not exist at redis database
        // got to mysql database search refresh token
        switch (from) {
            case from_pc:
                oneByTokenKey = refreshTokenDao.findOneByTokenKey(tokenKey);
                break;
            case from_mobile:
                oneByTokenKey = refreshTokenDao.findOneByMobileTokenKey(tokenKey);
                break;
            default:
                throw new IllegalStateException("Unexpected value: " + from);
        }

        if (oneByTokenKey == null) {
            System.out.println("用户没有登录过");
            return null;
        }


        // 使用 jwt 的时间字段限制登录缓存时间 一年
        try {
            Claims claims = JWTUtil.parseJWT(oneByTokenKey.getRefreshToken());
        } catch (ExpiredJwtException _e) {
            _e.printStackTrace();
            System.out.println("上次登录是一年前，需要重新登录");
            return null;
        }

        // complete return data
        String userID = oneByTokenKey.getUserID();
        User user = userDao.findOneById(userID);

        // change cache (redis and cookie)
        createToken(_response, user, from);

        return user;
    }

    /**
     * 获取用户信息
     *
     * @param _userId 用户id
     * @return 自定义的返回对象
     */
    @Override
    public ResponseResult getUserInfoById(String _userId) {
        // check data
        User oneById = userDao.findOneById(_userId);

        return getUserInfoCommonCode(oneById);
    }

    /**
     * 更新用户信息
     *
     * @param _userId 用户id
     * @param _user   用户user对象
     * @return 自定义的返回结果对象
     */
    @Override
    public ResponseResult updateUserInfoByUserId(String _userId, User _user) {
        HttpServletRequest _request = getRequest();

        // check env
        // The [useIsLogin] is from the redis database,it info is not completed,
        // so we need search from the mysql database to get the all info
        User userIsLogin = checkUserIsLogin();

        if (userIsLogin == null) {
            return ResponseResult.failed(ResponseState.user_not_login_message, ResponseState.user_not_login_code);
        }

        // 允许管理员更改大部分用户的信息
        if (!userIsLogin.getRoles().equals(constant.DB_ROLE_ADMIN)) {
            if (!userIsLogin.getId().equals(_userId)) {
                return ResponseResult.failed(ResponseState.no_power_message, ResponseState.no_power_code);
            }
        }

        User oneById = userDao.findOneById(_userId);

        // check data
        // check if the username is unique
        if (!TextUtil.isEmpty(_user.getUserName())) {
            ResponseResult usernameUnique = checkUsernameUnique(_user.getUserName(), _userId);

            if (usernameUnique.getCode() == ResponseState.username_not_unique_code) {
                return usernameUnique;
            }

            oneById.setUserName(_user.getUserName());
        }

        // complete data
        if (!TextUtil.isEmpty(_user.getAvatar())) {
            oneById.setAvatar(_user.getAvatar());
        }

        if (!TextUtil.isEmpty(_user.getPassword())) {
            oneById.setPassword(bCryptPasswordEncoder.encode(_user.getPassword()));
        }

        if (_user.getState() != null) {
            oneById.setState(_user.getState());
        }

        oneById.setSign(_user.getSign())
                .setUpdateTime(new Date());

        // change database state
        userDao.save(oneById);

        // delete the user's token key at redis database
        // the next time get the user info will from the mysql data
        // and will create a new token at the redis database for the user
        String tokenKey = CookieUtil.getCookieValue(_request, constant.cookie_token_key);

        redisUtil.del(constant.REDIS_TOKEN_KEY + tokenKey);

        return ResponseResult.success();
    }

    /**
     * 封禁用户， 需要管理员权限
     *
     * @param _request  请求体
     * @param _response 响应体
     * @param _userId   用户id
     * @return 自定义的返回结果对象
     */
    @Override
//    @Transactional
    public ResponseResult deleteUserByUserId(HttpServletRequest _request, HttpServletResponse _response, String _userId) {
        // change database state
        int i = userDao.updateUserStateById(constant.db_user_ban_state, _userId);

        if (i == 0) {
            return ResponseResult.failed(ResponseState.user_not_register_message, ResponseState.user_not_register_code);
        }

        return ResponseResult.success();
    }

    /**
     * 获取用户列表
     *
     * @param _page     页码
     * @param _size     页面大小
     * @param _userName 用户名
     * @param _email    邮箱
     * @return 自定义的返回结果对象
     */
    @Override
    public ResponseResult listUsers(int _page, int _size, String _userName, String _email) {

        if (_page < IUserService.constant.list_default_page_num) {
            _page = constant.list_default_page_num;
        }

        if (_size < constant.list_min_page_size) {
            _size = constant.list_min_page_size;
        }

        // complete data from mysql database
        Sort sort = new Sort(Sort.Direction.DESC, "createTime");

        Pageable pageable = PageRequest.of(_page - 1, _size, sort);


        Page<User> userDaoAll = userNoPasswordDao.findAll(new Specification<User>() {
            private static final long serialVersionUID = 7183485640145045405L;

            @Override
            public Predicate toPredicate(Root<User> _root, CriteriaQuery<?> _criteriaQuery, CriteriaBuilder _criteriaBuilder) {
                List<Predicate> predicates = new ArrayList<>();
                if (TextUtil.notEmpty(_userName)) {
                    Predicate userName = _criteriaBuilder.like(_root.get("userName").as(String.class), "%" + _userName + "%");

                    predicates.add(userName);
                }

                if (TextUtil.notEmpty(_email)) {
                    Predicate email = _criteriaBuilder.equal(_root.get("email").as(String.class), _email);
                    predicates.add(email);

                }
                Predicate[] predicates1 = new Predicate[predicates.size()];

                predicates.toArray(predicates1);

                return _criteriaBuilder.and(predicates1);
            }
        }, pageable);

        return ResponseResult.success().setData(userDaoAll);
    }


    /**
     * 获取用户信息的公共代码
     * 方便以后扩展 用 [email] [username] 等唯一值 查找用户
     *
     * @param _oneById 查找回来的用户对象
     * @return 自定义的返回对象
     */
    private ResponseResult getUserInfoCommonCode(User _oneById) {
        if (_oneById == null) {
            return ResponseResult.failed(ResponseState.user_not_register_message, ResponseState.user_not_register_code);
        }

        // complete data
        String userJsonStr = gson.toJson(_oneById);
        User user = gson.fromJson(userJsonStr, User.class);

        user.setPassword("")
                .setRegIp("")
                .setLoginIp("");
        // return data
        return ResponseResult.success().setData(user);
    }

    /**
     * 获取二维码
     *
     * @return 自定义返回对象
     */
    @Override
    public ResponseResult getPCLoginQrCode() {
        // check data
        String getQrCodeFrequentFlag = CookieUtil.getCookieValue(getRequest(), cookie_last_get_qr_code_flag);

        long code;
        if (TextUtil.notEmpty(getQrCodeFrequentFlag)) {
            code = Long.parseLong(getQrCodeFrequentFlag);

        } else {
            code = snowflakeIdWorker.nextId();
        }
        // complete data

        redisUtil.set(redis_pc_scan_login_key + code, String.valueOf(false), LRConstants.TimeValue.min_s * 5);

        // complete return data
        Map<String, String> result = new HashMap<>();

        result.put("code", String.valueOf(code));
        result.put("url", TextUtil.getDomain(getRequest()) + "/portal/image/qr-code/" + code);

        // 放在频繁获取的功能 使用 cookie 做 flag 标识
        CookieUtil.setUpCookie(getResponse(), cookie_last_get_qr_code_flag, String.valueOf(code));

        return ResponseResult.success().setData(result);
    }

    /**
     * 检查用户扫二维码的逻辑流程
     *
     * @param _qrCode 二维码对应的唯一值
     *                该值对应 redis 里面的一个flag
     * @return 自定义的返回对象
     */
    @Override
    public ResponseResult checkQrCodeLoginState(String _qrCode) {

        // check data
        ResponseResult responseResult = checkLoginIdState(_qrCode);

        if (responseResult != null) {
            return responseResult;
        }

        // 还未扫码登录 且二维码未过期
        // 先等待一会， 如果用户扫码成功就返回成功
        // 如果超时未扫码就显示等待扫码
        Callable<ResponseResult> callable = new Callable<ResponseResult>() {
            @Override
            public ResponseResult call() {
                // 先阻塞
                try {
                    countDownLatchManager.getLatch(_qrCode).await(30, TimeUnit.SECONDS);
                } catch (InterruptedException _e) {
                    _e.printStackTrace();
                }
                // 收到状态更新的通知，
                // 我们就检查二维码 对应的 登录 flag 的值
                // check data
                ResponseResult checkLoginIdState = checkLoginIdState(_qrCode);

                if (checkLoginIdState != null) {
                    return checkLoginIdState;
                }

                // 删除这个阻塞任务
                countDownLatchManager.deleteLatch(_qrCode);

                return ResponseResult.failed(ResponseState.qr_code_is_waiting_message, ResponseState.qr_code_is_waiting_code);
            }
        };

        try {
            callable.call();
        } catch (Exception _e) {
            _e.printStackTrace();
        }

        return ResponseResult.failed(ResponseState.qr_code_is_waiting_message, ResponseState.qr_code_is_waiting_code);
    }

    /**
     * 检查二维码对应的 唯一 flag 的 状态
     *
     * @param _qrCode 二维码对应的 唯一 flag
     * @return 自定义的返回对象
     */
    public ResponseResult checkLoginIdState(String _qrCode) {
        // loginState 有两种状态
        // 1 .false 未扫码登录
        // 2. 用户id 扫码登录成功
        String loginState = (String) redisUtil.get(redis_pc_scan_login_key + _qrCode);

        if (TextUtil.isEmpty(loginState)) {
            return ResponseResult.failed(ResponseState.qr_code_is_over_message, ResponseState.qr_code_is_over_code);
        }

        if (!loginState.equals(String.valueOf(false))) {

            createToken(getResponse(), userDao.findOneById(loginState), from_pc);
            // 扫码登录成功
            return ResponseResult.success();
        }

        return null;
    }

    /**
     * 更新二维码的状态
     *
     * @param _loginId 二维码对应的 唯一 标识登录状态的 flag
     * @return 自定义的返回对象
     */
    @Override
    public ResponseResult updateQrCodeLoginState(String _loginId) {
        // check data
        User userIsLogin = checkUserIsLogin();

        if (userIsLogin == null) {
            return ResponseResult.failed(ResponseState.user_not_login_message, ResponseState.user_not_login_code);
        }
        // complete data
        // update data
        redisUtil.set(redis_pc_scan_login_key + _loginId, userIsLogin.getId());

        countDownLatchManager.onPhoneDoLogin(_loginId);
        // complete return data
        // return data
        return ResponseResult.success();
    }

    /**
     * 获取网站注册人数
     *
     * @return 自定义返回对象
     */
    @Override
    public ResponseResult getRegisterCount() {
        long count = userDao.count();

        return ResponseResult.success().setData(count);
    }

    /**
     * 检查邮箱验证码是否正确
     *
     * @param _email      邮箱地址
     * @param _verifyCode 邮箱验证码
     * @return 自定义返回对象
     */
    @Override
    public ResponseResult checkEmailVerifyCode(String _email, String _verifyCode) {
        // check env
        String verifyValue = (String) redisUtil.get(constant.REDIS_KEY_EMAIL_SEND_VERIFY_CONTENT + _email);

        if (verifyValue != null) {
            if (verifyValue.equals(_verifyCode)) {
                return ResponseResult.success();
            }
        }

        return ResponseResult.failed();
    }

    /**
     * 检查人类验证码是否正确
     *
     * @param _captchaCode 人类验证码值
     * @return 自定义返回对象
     */
    @Override
    public ResponseResult checkCaptchaCode(String _captchaCode) {
        String captchaKey = CookieUtil.getCookieValue(getRequest(), cookie_last_captcha_id);
        if (captchaKey != null) {
            String captchaValue = (String) redisUtil.get(constant.REDIS_KEY_CAPTCHA_CONTENT + captchaKey);

            if (captchaValue != null) {
                if (captchaValue.equals(_captchaCode)) {
                    return ResponseResult.success();
                }
            }
        }

        return ResponseResult.failed();
    }

    /**
     * 退出登录
     *
     * @return 自定义返回对象
     */
//    @Transactional
    @Override
    public ResponseResult logout() {
        // check data
        String tokenKey = CookieUtil.getCookieValue(getRequest(), constant.cookie_token_key);

        if (tokenKey == null) {
            return ResponseResult.failed(ResponseState.user_not_login_message, ResponseState.user_not_login_code);
        }

        String from = tokenKey.split("_")[0] + "_";
        // change  data
        if (from_pc.equals(from)) {
            refreshTokenDao.deletePcTokenKey(tokenKey);
        } else {
            refreshTokenDao.deleteMobileToken(tokenKey);
        }

        redisUtil.del(constant.REDIS_TOKEN_KEY + tokenKey);
        CookieUtil.deleteCookie(getResponse(), constant.cookie_token_key);

        return ResponseResult.success();
    }

    /**
     * 获取登录着自己的信息
     *
     * @return 自定义返回对象
     */
    @Override
    public ResponseResult getOwnInfo() {
        User user = checkUserIsLogin();

        if (user == null) {
            return ResponseResult.failed(ResponseState.user_not_login_message, ResponseState.user_not_login_code);
        }

        return ResponseResult.success().setData(user);
    }

    private static HttpServletResponse getResponse() {
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        assert servletRequestAttributes != null;
        return servletRequestAttributes.getResponse();
    }

    private static HttpServletRequest getRequest() {
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        assert servletRequestAttributes != null;
        return servletRequestAttributes.getRequest();
    }
}
